//
// Created by mongobaba on 2021/4/19.
//

#ifndef UNIT_TEST_H
#define UNIT_TEST_H

#include <stdexcept>
#include <iostream>
#include <sstream>
#include <map>
#include <cstring>
#include <memory>

class assert_error : public std::logic_error {
public:
    explicit assert_error(const std::string &m) : std::logic_error(m) {}
};

template<typename T>
void throw_not_equal_error(const T &expected, const T &actual, const char *f, int line) {
    std::stringstream ss;
    ss << "assert fail in file " << f << ", line " << line << std::endl;
    ss << "\tExpected: " << expected << std::endl;
    ss << "\tActual: " << actual << std::endl;
    throw assert_error(ss.str());
}

template<typename T>
void assert_equal(const T &expected, const T &actual, const char *f, int line) {
    if (!(expected == actual)) {
        throw_not_equal_error(expected, actual, f, line);
    }
}

inline void assert_equal(const char *expected, const char *actual, const char *f, int line) {
    if (std::strcmp(expected, actual) != 0) {
        throw_not_equal_error(expected, actual, f, line);
    }
}

template<typename T>
void throw_equal_error(const T &left, const T &right,
                       const char *s1, const char *s2,
                       const char *f, int line) {
    std::stringstream ss;
    ss << "assert fail in file " << f << ", line " << line << std::endl;
    ss << "\tExpected: " << s1 << " != " << s2 << ", but they are all " << left << std::endl;
    throw assert_error(ss.str());
}

template<typename T>
void assert_not_equal(const T &left, const T &right,
                      const char *s1, const char *s2,
                      const char *f, int line) {
    if (left == right) {
        throw_equal_error(left, right, s1, s2, f, line);
    }
}

inline void assert_not_equal(const char *left, const char *right,
                             const char *s1, const char *s2,
                             const char *f, int line) {
    if (std::strcmp(left, right) == 0) {
        throw_equal_error(left, right, s1, s2, f, line);
    }
}

#define ASSERT_EQ(expected, actual) assert_equal((expected), (actual), __FILE__, __LINE__)
#define ASSERT_NE(left, right) assert_not_equal((left), (right), #left, #right, __FILE__, __LINE__)

class basic_test;

class test_group {
    using function_type = basic_test *(*)();
public:
    static test_group *get_all_tests() {
        static test_group group;
        return &group;
    }

    void register_class(std::string name, function_type f) {
        auto it = creators_.find(name);
        if (it == creators_.end()) {
            creators_.emplace(std::move(name), f);
        }
    }

    size_t run();

private:
    std::map<std::string, function_type> creators_;
};

class basic_test {
public:
    explicit basic_test(std::string name) : name_(std::move(name)) {}

    virtual ~basic_test() = default;

    virtual void run() = 0;

    const std::string &get_name() const { return name_; }

private:
    std::string name_;
};

inline size_t test_group::run() {
    size_t succeed = 0;
    size_t failed = 0;
    for (auto &p : creators_) {
        std::shared_ptr<basic_test> test((p.second)());
        std::cout << "run test " << test->get_name() << std::endl;
        try {
            test->run();
            ++succeed;
        } catch (assert_error &e) {
            std::cout << e.what();
            ++failed;
        }
    }
    if (failed == 0) {
        std::cout << "!!! all tests passed !!!" << std::endl;
    } else {
        std::cout << succeed << " tests passed, " << failed << " tests failed" << std::endl;
    }
    return failed;
}

#define TEST(name) \
class name##_test : public basic_test { \
public: \
    name##_test() : basic_test(#name) {} \
    virtual void run(); \
    static basic_test* create() { return new name##_test(); } \
}; \
namespace name##_test_impl_should_not_be_used { \
    struct name##_test_register { \
        name##_test_register() noexcept { \
            test_group::get_all_tests()->register_class(#name, &name##_test::create); \
        } \
    } instance; \
} \
void name##_test::run()

#define RUN_ALL_TESTS() test_group::get_all_tests()->run()

#endif //UNIT_TEST_H
